Анализ поведения пользователей мобильного приложения по продаже продуктов питания¶

Описание проекта¶

Цель:

Проанализировать поведение пользователей приложения на основе записей в логе и результатов А/В-эксперимента.

Задачи:

  • Изучить воронку продаж и узнать как пользователи доходят до покупки. Узнать сколько пользователей доходит до покупки, а сколько "застревает" на предыдущих шагах и на каких именно;
  • Исследовать результаты А/В-эксперимента и как влияет изменение шрифта во всем приложении на пользователей.

Ход работы:

  • Считать данные из файла;
  • Провести предобработку данных для работы: проверить пропуски, типы данных, названия столбцов;
  • Изучить и проверить данные:
    • сколько всего событий в логе,
    • сколько всего пользователей в логе,
    • за какой период представлены данные;
  • Изучить воронку событий:
    • посмотреть события в логах и частоту их встречаемости,
    • посчитать сколько пользователей совершали каждое из событий,
    • определить порядок происхождения событий,
    • посчитать, какая доля пользователей проходит на следующий шаг воронки,
    • определить, на каком шаге теряется больше всего пользователей,
    • определить, какая доля пользователей доходит от первого события до продажи;
  • Изучить результаты эксперимента:
    • посчитать, сколько пользователей в каждой группе эксперимента,
    • посчитать число пользователей и их долю, совершивших самое популярное событие,
    • сравнить результаты эксперимента в разных группах,
    • сделать выводы из эксперимента;
  • Вывод.

Считываем данные из файла и подключаем библиотеки¶

Подключим необходимые библиотеки.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from scipy import stats as st
import plotly.express as px
import plotly.graph_objs as go
import math as mth
import warnings

Считаем данные из файла с логами пользователей.

In [2]:
#Определим класс с цветами для использования в работе
class color:
    bold = '\033[1m'
    red = '\033[91m'
    end = '\033[0m'
In [3]:
#Считаем данные из файла
data = pd.read_csv('dataset/logs_exp.csv', sep='\t')
data
Out[3]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
... ... ... ... ...
244121 MainScreenAppear 4599628364049201812 1565212345 247
244122 MainScreenAppear 5849806612437486590 1565212439 246
244123 MainScreenAppear 5746969938801999050 1565212483 246
244124 MainScreenAppear 5746969938801999050 1565212498 246
244125 OffersScreenAppear 5746969938801999050 1565212517 246

244126 rows × 4 columns

In [4]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB

При первичном изучении данных, заметим, что:

  • Данные не содержат пропусков, так как количество ненулевых элементов во всех столбцах одинаково: 244126;
  • Названия столбцов не соответствуют snake_case, поэтому далее необходимо будет привести названия столбцов к соответствующему виду;
  • Необходимо будет создать новый столбец типа datetime и занести в него дату события из столбца EventTimestamp, для более удобной работы, так как сейчас дата в этом столбце записана в секундах;
  • Необходимо будет провести проверку на дубликаты.

Предобработка данных¶

Приведем названия столбцов к соответствующему виду.

In [5]:
#Переименуем столбцы
data = data.rename(columns={'EventName':'event_name', 'DeviceIDHash':'user_id',\
                            'EventTimestamp':'event_timestamp', 'ExpId':'group_id'})
data.head()
Out[5]:
event_name user_id event_timestamp group_id
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
In [6]:
#Заменим тип столбца с датой события
data['event_timestamp'] = pd.to_datetime(data['event_timestamp'], unit='s')

#Добавим столбец с датой события
data['date'] = pd.to_datetime(data['event_timestamp'].dt.date)
data.head()
Out[6]:
event_name user_id event_timestamp group_id date
0 MainScreenAppear 4575588528974610257 2019-07-25 04:43:36 246 2019-07-25
1 MainScreenAppear 7416695313311560658 2019-07-25 11:11:42 246 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 2019-07-25 11:28:47 248 2019-07-25
3 CartScreenAppear 3518123091307005509 2019-07-25 11:28:47 248 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 2019-07-25 11:48:42 248 2019-07-25
In [7]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 5 columns):
 #   Column           Non-Null Count   Dtype         
---  ------           --------------   -----         
 0   event_name       244126 non-null  object        
 1   user_id          244126 non-null  int64         
 2   event_timestamp  244126 non-null  datetime64[ns]
 3   group_id         244126 non-null  int64         
 4   date             244126 non-null  datetime64[ns]
dtypes: datetime64[ns](2), int64(2), object(1)
memory usage: 9.3+ MB

Поменяли названия столбцов, тип столбца с датой и временем события, а также добавили новый столбец с датой события.

Проверим количество пропущенных значений в столбцах.

In [8]:
data.isna().sum()
Out[8]:
event_name         0
user_id            0
event_timestamp    0
group_id           0
date               0
dtype: int64
In [9]:
data.duplicated().sum()
Out[9]:
413

В таблице присутствует 413 дубликатов.

Запомним размер датафрейма в переменной, перед тем как приступим к удалению дубликатов и проведению других манипуляций.

In [10]:
data_len_original = len(data)
print('Размер таблицы до проведения манипуляций:', data_len_original)
Размер таблицы до проведения манипуляций: 244126
In [11]:
data = data.drop_duplicates().reset_index(drop=True)
In [12]:
data_len_after_manipulation = len(data)
print('Размер таблицы после проведения манипуляций:', data_len_after_manipulation)
print('Было удалено', round((data_len_original/data_len_after_manipulation * 100 -100), 2), '% данных')
Размер таблицы после проведения манипуляций: 243713
Было удалено 0.17 % данных

Для дальнейшей работы важно знать, что пользователи в группах не пересекаются. Проверим, есть ли пользователи, которые одновременно находятся в двух или более группах.

In [13]:
#Выведем уникальные названия групп
print(color.bold + 'Уникальных группы:' + color.end, data['group_id'].nunique(), ', а именно:', data['group_id'].unique())
print(color.bold + 'Уникальных события:' + color.end, data['event_name'].nunique(), ', а именно:', \
      data['event_name'].unique())
Уникальных группы: 3 , а именно: [246 248 247]
Уникальных события: 5 , а именно: ['MainScreenAppear' 'PaymentScreenSuccessful' 'CartScreenAppear'
 'OffersScreenAppear' 'Tutorial']
In [14]:
#Проверяем, есть ли пользователи, находящиеся в нескольких группах одновременно
groups_by_id = data.groupby('user_id')['group_id'].nunique().reset_index()

# print(groups_by_id['group_id'])
count_index = 0
for i in groups_by_id['group_id']:
    if i > 1:
        print(data['user_id'])
    else:
        count_index +=1
if len(groups_by_id) == count_index:
    print('Нет пользователей, одновременно находящихся в более чем 1 группе')
Нет пользователей, одновременно находящихся в более чем 1 группе

Изучение и проверка данных¶

Проверим, сколько всего событий в логе, сколько всего пользователей в логе, сколько в среднем событий приходится на пользователя.

In [15]:
print(color.bold +'Всего событий в логе:'+color.end, data.shape[0])
print(color.bold +'Всего уникальных видов событий:'+color.end, data['event_name'].nunique())
print(color.bold +'Всего уникальных пользователей:'+color.end, data['user_id'].nunique())
print(color.bold +'В среднем на пользователя приходится'+color.end,\
      round(data.groupby('user_id')['event_name'].count().mean()), color.bold +'события'+color.end)
Всего событий в логе: 243713
Всего уникальных видов событий: 5
Всего уникальных пользователей: 7551
В среднем на пользователя приходится 32 события

Посмотрим более наглядно на то, сколько событий совершают пользователи.

In [16]:
event_by_user = data.groupby('user_id')['event_name'].count().reset_index()
event_by_user.columns = ['user_id', 'event_count']
round(event_by_user['event_count'].describe(), 2)
Out[16]:
count    7551.00
mean       32.28
std        65.15
min         1.00
25%         9.00
50%        20.00
75%        37.00
max      2307.00
Name: event_count, dtype: float64
In [17]:
plt.figure(figsize=(10, 5))
sns.histplot(event_by_user['event_count'], bins=50, kde=True)
plt.title('Распределение количества событий на пользователя')
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.grid(which='major')
plt.show()

Как видно из графика и описания, в среднем на пользователя приходится 32 события, при этом медианное значение - 20 событий на пользователя. Минимальное количество событий равно 1, а максимальное количество событий равно 2307.

Определим за какой период у нас представлены данные в таблице.

In [18]:
warnings.filterwarnings('ignore')
data['date'].describe()
Out[18]:
count                  243713
unique                     14
top       2019-08-01 00:00:00
freq                    36141
first     2019-07-25 00:00:00
last      2019-08-07 00:00:00
Name: date, dtype: object

В датафрейме представлены данные за период с 2019-07-25 по 2019-08-07. Посмотрим как распределены данные по этому периоду. Является ли распределение одинаково полным за весь период.

In [19]:
plt.title('Распределение логов по дате и времени')
plt.xlabel('Дата')
plt.ylabel('Кол-во логов')
data['event_timestamp'].hist(bins=100, figsize=(20, 5), ec="yellow", fc="green", alpha=0.6, linewidth=2)

plt.show()

plt.title('Распределение логов по времени суток')
plt.xlabel('Дата')
plt.ylabel('Кол-во логов')
data['event_timestamp'].dt.hour.hist(bins=24, figsize=(20, 5),\
                                    ec="yellow", fc="green", alpha=0.6, linewidth=2)
plt.xticks(range(0, 23))
plt.show()

На первом графике видно, что данных до августа практически нет, это может искажать наши последующие вычисления, поэтому лучше взять данных начиная с 2019-08-01 до 2019-08-07.

На втором графике распределения логов по часам в сутках видим, что наивысшая активность пользователей приходится на 14-15 часов, в то время как наименьшая активность происходит ночью, начиная с 21 вечера.

Исходя из всего вышесказанного, отбросим старые данные и посмотрим, много ли событий и пользователей мы потеряем.

In [20]:
original_logs = data.shape[0]
original_users = data['user_id'].nunique()

#Отбросим старые данные 
data_new = data.query('event_timestamp >= "2019-08-01"')
changed_logs = data_new.shape[0]
changed_users = data_new['user_id'].nunique()

#Посмотрим как изменились количество событий и пользователей
print('Количество логов до корректировки:', original_logs)
print('Количество логов после корректировки:', changed_logs)
print(color.bold +'После удаления старых данных количество логов сократилось на' + color.end,\
      round(original_logs/changed_logs * 100 - 100, 2), '%, то есть на', original_logs - changed_logs, 'логов\n')

print('Количество уникальных пользователей до корректировки:', original_users)
print('Количество уникальных пользователей после корректировки:', changed_users)
print(color.bold +'После удаления старых данных количество уникальных пользователей сократилось на' + color.end,\
      round(original_users/changed_users * 100 - 100, 2), '%, то есть на', original_users - changed_users, 'пользователей')
Количество логов до корректировки: 243713
Количество логов после корректировки: 240887
После удаления старых данных количество логов сократилось на 1.17 %, то есть на 2826 логов

Количество уникальных пользователей до корректировки: 7551
Количество уникальных пользователей после корректировки: 7534
После удаления старых данных количество уникальных пользователей сократилось на 0.23 %, то есть на 17 пользователей

Теперь проверим, что после удаления старых данных во всех трех экспериментальных группах есть пользователи.

In [21]:
group_table = data_new.groupby('group_id').agg({'user_id':['nunique','count']}).reset_index()
group_table.columns = ['Группа', 'Кол-во пользователей', 'Кол-во логов']
group_table
Out[21]:
Группа Кол-во пользователей Кол-во логов
0 246 2484 79302
1 247 2513 77022
2 248 2537 84563

Во всех трех группах количество пользователей примерно одинаковое. Количество событий наибольшее у группы 248.

Воронка событий¶

Посмотрим, какие события есть в логах и как часто они встречаются.

In [22]:
events = data_new.groupby('event_name')['user_id'].count().reset_index().sort_values(by='user_id', ascending=False)
events.columns = ['Наименование события', 'Количество событий']
display(events)
Наименование события Количество событий
1 MainScreenAppear 117328
2 OffersScreenAppear 46333
0 CartScreenAppear 42303
3 PaymentScreenSuccessful 33918
4 Tutorial 1005

Построим график, чтобы визуально увидеть: насколько отличается количество событий по разным событиям.

In [23]:
plt.figure(figsize=(17, 6))
sns.barplot(data=events, x='Количество событий', y='Наименование события', color='blue', alpha=0.5)
for i, v in enumerate(events['Количество событий']):
    plt.text(v+3000, i, str(round(v, 2)), ha = 'center', size = 12)
plt.title('Частота событий', fontsize=14)
plt.ylabel('Наименование событий', fontsize=14)
plt.xlabel('Количество событий', fontsize=14)
plt.show()

Наибольшее количество событий приходится на MainScreenAppear, а наименьшее количество событий приходится на Tutorial. OffersScreenAppear, CartScreenAppear, PaymentScreenSuccessful находятся на примерно на одном уровне по количеству событий.

Посмотрим теперь какое количество уникальных пользователей совершает события и посчитаем долю пользователей, которые совершали событие.

In [24]:
users = data_new.groupby('event_name')['user_id'].nunique().reset_index().sort_values(by='user_id', ascending=False)
users['part'] = round(users['user_id'] / data_new['user_id'].nunique() * 100, 2) 

users.columns = ['Наименование события', 'Количество событий', 'Доля пользователей']
display(users)
Наименование события Количество событий Доля пользователей
1 MainScreenAppear 7419 98.47
2 OffersScreenAppear 4593 60.96
0 CartScreenAppear 3734 49.56
3 PaymentScreenSuccessful 3539 46.97
4 Tutorial 840 11.15

Наибольшая доля пользователей увидели MainScreenAppear, а наименьшая Tutorial. Видно, что постепенно начиная с главной страницы события выстраиваются в воронку, начинается постепенный отток пользователей с каждым последующим кликом. Однако шаг Tutorial большинство пропускает. Это может быть связано с понятным интерфейсом и многие пропускают этот шаг. Также просмотр туториала не связан с совершением покупки и смотреть его необязательно.

Построим нашу получившуюся воронку.

In [25]:
funnel = users[users['Наименование события'] != 'Tutorial']

fig = go.Figure(go.Funnel(y=funnel['Наименование события'],
                          x=funnel['Количество событий'],
                          textposition='inside',
                          textinfo='value + percent previous',
                          textfont_size=14,
                          marker={"color":['#ed7953', '#fb9f3a', '#fdca26', '#f0f921']}
                         ))
fig.update_layout(
    title={'text':'Воронка событий',
           'y':0.9,
           'x':0.55,
           'xanchor':'center',
           'yanchor':'top'})
fig.show()
In [26]:
funnel['percent'] = round(100 + funnel['Количество событий'].pct_change().fillna(0) * 100, 2) 
funnel['percent_leave'] = round(funnel['Количество событий'].pct_change().fillna(0) * 100, 2)
funnel.rename(columns={'percent':'Воронка', 'percent_leave':'Процент оттока'}, inplace=True)
funnel
Out[26]:
Наименование события Количество событий Доля пользователей Воронка Процент оттока
1 MainScreenAppear 7419 98.47 100.00 0.00
2 OffersScreenAppear 4593 60.96 61.91 -38.09
0 CartScreenAppear 3734 49.56 81.30 -18.70
3 PaymentScreenSuccessful 3539 46.97 94.78 -5.22

Как видно из графика и таблицы большинство пользователей теряется после первого шага, что составляет 38.09% пользователей. Успешно оплачивают товары только 46.97% пользователей от общего числа уникальных пользователей.

Изучение результатов эксперимента¶

Проверим сколько пользователей в каждой экспериментальной группе.

Для А/А-эксперимента есть 2 контрольные группы, для проверки корректности всех механизмов и расчетов проверим, находят ли статистические критерии разницу между выборками 246 и 247.

In [27]:
#Узнаем сколько пользователей в каждой экспериментальной группе
table_groups = data_new.groupby('group_id')['user_id'].nunique().reset_index()
table_groups.columns = ['Группа', 'Кол-во пользователей']
table_groups
Out[27]:
Группа Кол-во пользователей
0 246 2484
1 247 2513
2 248 2537
In [28]:
funnel_by_groups = data_new.groupby(['event_name', 'group_id'])['user_id']\
.nunique().reset_index().sort_values(by=['group_id', 'user_id'], ascending=False)

funnel_by_groups = funnel_by_groups[funnel_by_groups['event_name'] != 'Tutorial']
funnel_by_groups['part'] = round(funnel_by_groups['user_id'] / data_new['user_id'].nunique() * 100, 2) 

funnel_by_groups.columns = ['Наименование события', 'Группа', 'Кол-во пользователей', 'Доля пользователей']

funnel_by_groups['percent'] = round(100 + funnel_by_groups.groupby('Группа')['Кол-во пользователей']\
                                    .pct_change().fillna(0) * 100, 2) 
funnel_by_groups['percent_leave'] = round(funnel_by_groups.groupby('Группа')['Кол-во пользователей']\
                                          .pct_change().fillna(0) * 100, 2)
funnel_by_groups.rename(columns={'percent':'Воронка', 'percent_leave':'Процент оттока'}, inplace=True)


cm = sns.light_palette("lightblue", as_cmap=True)

funnel_by_groups.style.background_gradient(cmap=cm)
Out[28]:
  Наименование события Группа Кол-во пользователей Доля пользователей Воронка Процент оттока
5 MainScreenAppear 248 2493 33.090000 100.000000 0.000000
8 OffersScreenAppear 248 1531 20.320000 61.410000 -38.590000
2 CartScreenAppear 248 1230 16.330000 80.340000 -19.660000
11 PaymentScreenSuccessful 248 1181 15.680000 96.020000 -3.980000
4 MainScreenAppear 247 2476 32.860000 100.000000 0.000000
7 OffersScreenAppear 247 1520 20.180000 61.390000 -38.610000
1 CartScreenAppear 247 1238 16.430000 81.450000 -18.550000
10 PaymentScreenSuccessful 247 1158 15.370000 93.540000 -6.460000
3 MainScreenAppear 246 2450 32.520000 100.000000 0.000000
6 OffersScreenAppear 246 1542 20.470000 62.940000 -37.060000
0 CartScreenAppear 246 1266 16.800000 82.100000 -17.900000
9 PaymentScreenSuccessful 246 1200 15.930000 94.790000 -5.210000
In [29]:
fig = go.Figure()

fig.add_trace(go.Funnel(name='248',
                        y=funnel_by_groups.query('Группа == 248')['Наименование события'],
                        x=funnel_by_groups.query('Группа == 248')['Кол-во пользователей'],
                        textposition='inside',
                        textinfo='value + percent previous',
                        marker={"color":['#0d0887', '#46039f', '#7201a8', '#9c179e']}
                       ))

fig.add_trace(go.Funnel(name='247',
                        y=funnel_by_groups.query('Группа == 247')['Наименование события'],
                        x=funnel_by_groups.query('Группа == 247')['Кол-во пользователей'],
                        textposition='inside',
                        textinfo='value + percent previous',
                        marker={"color":[ '#ed7953', '#fb9f3a', '#fdca26', '#f0f921']}
                       ))

fig.add_trace(go.Funnel(name='246',
                        y=funnel_by_groups.query('Группа == 246')['Наименование события'],
                        x=funnel_by_groups.query('Группа == 246')['Кол-во пользователей'],
                        textposition='inside',
                        textinfo='value + percent previous',
                        marker={"color":[ '#9c179e', '#bd3786', '#d8576b', '#ed7953']}
                       ))
                        
fig.update_layout(
    title={'text':'Воронка событий в экспериментальных группах',
           'y':0.9,
           'x':0.55,
          'xanchor':'center',
          'yanchor':'top'})
fig.show()

Количество пользователей в разных группах разнится ненамного. Проверим находят ли статистические критерии разницу между выборками групп 246 и 247, принимающих участие в А/А-эксперименте.

Необходимо будет сравнить доли пользователей по каждому событию в каждой группе, узнать отличаются ли эти доли или же мы можем утверждать, что между ними нет разницы.

  • необходимо сравнить группы 246 и 247
  • аналогично сравнить группу с измененным шрифтом (248) с каждой из контрольных групп (246 и 247) по каждому событию
  • сравнить результаты с объединенной контрольной группой (246+247 и 248)

Для этого сформируем гипотезы:

  • Нулевая: Доли уникальных пользователей, совершивших событие на этапе воронки, равны

  • Альтернативная: Доли уникальных пользователей, совершивших событие на этапе воронки, разные

Для данного анализа будем использовать метод Шидака для снижения вероятности ложнопозитивного результата, при множественном тестировании гипотез.

In [30]:
group_one = data_new[data_new['group_id'] == 246]
group_two = data_new[data_new['group_id'] == 247]
group_tree = data_new[data_new['group_id'] == 248]
group_one_two_combined = data_new[data_new['group_id'] != 248]

data_new = data_new[data_new['event_name'] != "Tutorial"]

def tests(g1, g2, event, m):    
    #Уровень статистической значимости
    alpha = 0.05
    #Используем метод Шидака 
    # m - кол-во сравнений для метода Шидака по формуле: 1 - (1 - alpha) ** 1/m
    alpha_shidak = 1 - (1 - alpha) ** (1/m)
    
    #Число пользователей совершивших события по группам
    successes = np.array([g1[g1['event_name'] == event]['user_id'].nunique(),
                         g2[g2['event_name'] == event]['user_id'].nunique()])
    
    #Общее кол-во пользователей в группах
    number_of_users = np.array([g1['user_id'].nunique(),
                               g2['user_id'].nunique()])
    
    #Пропорции успехов в 1 и 2 группах
    p1 = successes[0] / number_of_users[0]
    p2 = successes[1] / number_of_users[1]
    
    #Пропорция успехов в комбинированном датафрейме
    p_combined = (successes[0] + successes[1]) / (number_of_users[0] + number_of_users[1])
    
    #Разница пропорций
    difference = p1 - p2
    
    #Считаем статистику в ст. отклонениях стандартного норм. распределения
    z_value = difference / mth.sqrt(p_combined * (1 - p_combined) *\
                                    (1 / number_of_users[0] + 1 / number_of_users[1]))
    
    #Задаем стандартное нормальное распределение
    distr = st.norm(0, 1)
    
    #Если бы пропорции были равны, разница между ними = 0, т.к. распределение норм., вызовем метод
    #cdf(), модуль abs() т.к. тест двусторонний, по этой же причине удваиваем результат
    p_value = (1 - distr.cdf(abs(z_value))) * 2
    
    print(color.bold +'Группы:' + color.end, g1['group_id'].unique(), 'и', g2['group_id'].unique())
    print(color.bold +'Событие' + color.end, event)
    print(color.bold +'p-значение:' + color.end, p_value)
    
    if p_value < alpha_shidak:
        print('Отвергаем нулевую гипотезу: между долями есть разница')
    else:
        print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
In [31]:
for event in data_new['event_name'].unique():
    tests(group_one, group_two, event, 4)
    print()
Группы: [246] и [247]
Событие MainScreenAppear
p-значение: 0.7570597232046099
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [247]
Событие OffersScreenAppear
p-значение: 0.2480954578522181
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [247]
Событие CartScreenAppear
p-значение: 0.22883372237997213
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [247]
Событие PaymentScreenSuccessful
p-значение: 0.11456679313141849
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Мы имеем по 4 гипотезы для каждого теста: А1/А2-тест, и для А/В-тестов: А1/В, А2/В, А1+А2/В. Итого получается всего 16 проверок нулевых гипотез на одном наборе данных. Так как с каждой новой проверкой гипотезы растет групповая вероятность совершить ошибку первого рода (FWER), то есть выше вероятность, что хотя бы в одном из попарных сравнений будет зафиксирован ложнопозитивный результат (будет принята неверная гипотеза), то необходимо использовать метод Шидака для снижения вероятности ложнопозитивного результата со значением m = 4, так как m - это число тестируемых гипотез.

Различия между экспериментальными группами для А/А-теста не обнаружились, можем проверять гргуппу с измененным шрифтом по отдельности с каждой из контрольных групп.

In [32]:
for event in data_new['event_name'].unique():
    tests(group_one, group_tree, event, 4)
    print()
Группы: [246] и [248]
Событие MainScreenAppear
p-значение: 0.2949721933554552
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [248]
Событие OffersScreenAppear
p-значение: 0.20836205402738917
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [248]
Событие CartScreenAppear
p-значение: 0.07842923237520116
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246] и [248]
Событие PaymentScreenSuccessful
p-значение: 0.2122553275697796
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

In [33]:
for event in data_new['event_name'].unique():
    tests(group_two, group_tree, event, 4)
    print()
Группы: [247] и [248]
Событие MainScreenAppear
p-значение: 0.4587053616621515
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [247] и [248]
Событие OffersScreenAppear
p-значение: 0.9197817830592261
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [247] и [248]
Событие CartScreenAppear
p-значение: 0.5786197879539783
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [247] и [248]
Событие PaymentScreenSuccessful
p-значение: 0.7373415053803964
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

При проверки группы с измененным шрифтом и контрольных групп также не было оснований считать доли разными.

In [34]:
for event in data_new['event_name'].unique():
    tests(group_one_two_combined, group_tree, event, 4)
    print()
Группы: [246 247] и [248]
Событие MainScreenAppear
p-значение: 0.29424526837179577
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246 247] и [248]
Событие OffersScreenAppear
p-значение: 0.43425549655188256
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246 247] и [248]
Событие CartScreenAppear
p-значение: 0.18175875284404386
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Группы: [246 247] и [248]
Событие PaymentScreenSuccessful
p-значение: 0.6004294282308704
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Различия при сравнении группы с измененным шрифтом и объединенной контрольной группой не обнаружены. Следовательно, можно сделать вывод, что изменения шрифта в приложении не повлияло на поведение пользователей.

Вывод¶

В данном проекте было проанализированно поведения покупателей на основе логов пользователей мобильного приложения. Также была изучена воронка продаж, по которой двигаются пользователи до совершения покупки и исследованны результаты А/В-эксперимента по изменению шрифта в приложении.

Была проведена предобработка данных, изменены названия столбцов и типы данных, также было выяснено за какой период лучше брать данные, чтобы не получить искажение результатов. В данном случае был взят период с 2019-08-01 по 2019-08-07.

Было выявлено, что всего в приложении 5 события. Событие Tutorial было самое наименее просматриваемое, всего 11% пользователей совершило данное событие. Пользователи практические не совершали данное событие в виду того, что оно является не обязательным и не необходимым для совершения покупки в приложении.

  • Также 98% пользователей (7419) просмотрели главную страницу,
  • 60,96% пользователей (4593) смотрели страницу товара,
  • 49,56% пользователей (3734) смотрели карточку товара,
  • 46,97% пользователей (3539) совершили успешно оплату.

При исследовании были найдены события, на которых "отваливаются" большинсво пользователей, а именно, после первого шага оставалось только 62% пользователей, то есть 38% пользователей уходило из приложения. на последующих шагах потеря пользователей была минимальна, порядка 9% и 5%.

При анализе А/В-эксперимента было учтено поведение пользователей из 3 групп:

  • 246 группа с 2450 пользователями,
  • 247 группа с 2476 пользователями,
  • 248 группа с 2493 пользователями.

Согласно выдвинутой гипотезе необходимо было либо согласиться с ней и с тем, что доли пользователей по каждому событию в группах равны, либо отодвинуть ее в пользу альтернативной гипотезы о том, что доли разные.

При проведении А/В-тестов по каждому событию среди 2х контрольных групп (246 и 247), а также среди этих контрольных групп и экспериментально группы с изменением шрифта (248), не была обнаружена статистически значимая разница между долями пользователей в группах. Из чего следует сделать вывод, что изменение шрифта приложения не повлияло на поведение пользователей.